package com.rm.freedrawview;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.ComponentParent;
import ohos.agp.components.ScrollView;
import ohos.agp.render.*;
import ohos.agp.utils.Color;
import ohos.app.Context;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Size;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;
import ohos.utils.PacMap;
import ohos.utils.Sequenceable;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Optional;

/**
 * Created by Riccardo Moro on 9/10/2016.
 *
 * @author a
 * @version 1.0.0
 */
public class FreeDrawView extends Component implements Component.BindStateChangedListener, AbilityStateCall {
    private static final float DEFAULT_STROKE_WIDTH = 4;
    private static final int DEFAULT_COLOR = Color.BLACK.getValue();
    private static final int DEFAULT_ALPHA = 255;

    private Paint mCurrentPaint;
    private Path mCurrentPath;

    private ResizeBehaviour mResizeBehaviour;

    private ArrayList<Point> mPoints = new ArrayList<>();
    private ArrayList<HistoryPath> mPaths = new ArrayList<>();
    private ArrayList<HistoryPath> mCanceledPaths = new ArrayList<>();

    private int mPaintColor = DEFAULT_COLOR;
    private int mPaintAlpha = DEFAULT_ALPHA;

    private int mLastDimensionW = -1;
    private int mLastDimensionH = -1;

    private boolean mFinishPath = false;

    private PathDrawnListener mPathDrawnListener;
    private PathRedoUndoCountChangeListener mPathRedoUndoCountChangeListener;
    private ScrollView mScrollView;

    public FreeDrawView(Context context) {
        this(context, null);
    }

    public FreeDrawView(Context context, AttrSet attrSet) {
        this(context, attrSet, "");
    }


    public FreeDrawView(Context context, AttrSet attrs, String styleName) {
        super(context, attrs, styleName);

        setTouchEventListener(this::onTouchEvent);
        addDrawTask(this::onDraw);
        setBindStateChangedListener(this);
        initPaints(attrs);

    }

    @Override
    public void onSaveAbilityState(PacMap outState) {
        if (mPoints.size() > 0) {
            createHistoryPathFromPoints();
        }

        FreeDrawSavedState freeDrawSavedState = new FreeDrawSavedState(mPaths, mCanceledPaths,
                getPaintWidth(), getPaintColor(), getPaintAlpha(),
                getResizeBehaviour(), mLastDimensionW, mLastDimensionH);

        outState.putSequenceableObject("FreeDrawSavedState", freeDrawSavedState);
    }

    @Override
    public void onRestoreAbilityState(PacMap state) {
        Optional<Sequenceable> freeDrawSavedState = state.getSequenceable("FreeDrawSavedState");
        if (!freeDrawSavedState.isPresent()) {
            return;
        }
        Sequenceable sequenceable = freeDrawSavedState.get();

        if (!(sequenceable instanceof FreeDrawSavedState)) {
            return;
        }
        FreeDrawSavedState savedState = (FreeDrawSavedState) sequenceable;

        mPaths = savedState.getPaths();
        mCanceledPaths = savedState.getCanceledPaths();
        mCurrentPaint = savedState.getCurrentPaint();

        setPaintWidthPx(savedState.getCurrentPaintWidth());
        setPaintColor(savedState.getPaintColor());
        setPaintAlpha(savedState.getPaintAlpha());

        setResizeBehaviour(savedState.getResizeBehaviour());

        mLastDimensionW = savedState.getLastDimensionW();
        mLastDimensionH = savedState.getLastDimensionH();

        notifyRedoUndoCountChanged();
    }

    /**
     * Set the paint color
     *
     * @param color The now color to be applied to the
     */
    public void setPaintColor(int color) {

        invalidate();

        mPaintColor = color;

        mCurrentPaint.setColor(new Color(mPaintColor));
        mCurrentPaint.setAlpha(mPaintAlpha / 255f);
    }

    /**
     * Get the current paint color without it's alpha
     *
     * @return int
     */
    public int getPaintColor() {
        return mPaintColor;
    }

    /**
     * Get the current color with the current alpha
     *
     * @return int
     */
    public int getPaintColorWithAlpha() {
        return mCurrentPaint.getColor().getValue();
    }


    /**
     * Set the paint width in px
     *
     * @param widthPx The new weight in px, must be > 0
     */
    public void setPaintWidthPx(float widthPx) {
        if (widthPx > 0) {

            invalidate();

            mCurrentPaint.setStrokeWidth(widthPx);
        }
    }

    /**
     * Set the paint width in dp
     *
     * @param dp The new weight in dp, must be > 0
     */
    public void setPaintWidthDp(float dp) {
        setPaintWidthPx(FreeDrawHelper.convertDpToPixels(getContext(), dp));
    }

    /**
     * getPaintWidth
     * {@link #getPaintWidth(boolean)}
     *
     * @return int
     */
    public float getPaintWidth() {
        return getPaintWidth(false);
    }

    /**
     * Get the current paint with in dp or pixel
     *
     * @param inDp
     * @return float
     */

    public float getPaintWidth(boolean inDp) {
        if (inDp) {
            return FreeDrawHelper.convertPixelsToDp(getContext(), mCurrentPaint.getStrokeWidth());
        } else {
            return mCurrentPaint.getStrokeWidth();
        }
    }


    /**
     * Set the paint opacity, must be between 0 and 1
     *
     * @param alpha The alpha to apply to the paint 0-255
     */
    public void setPaintAlpha(int alpha) {
        invalidate();

        mPaintAlpha = alpha;
        mCurrentPaint.setAlpha(mPaintAlpha / 255f);
    }

    /**
     * Get the current paint alpha
     *
     * @return int
     */

    public int getPaintAlpha() {
        return mPaintAlpha;
    }


    /**
     * Set what to do when the view is resized (on rotation if its dimensions are not fixed)
     * {@link ResizeBehaviour}
     *
     * @param newBehaviour newBehaviour
     */
    public void setResizeBehaviour(ResizeBehaviour newBehaviour) {
        mResizeBehaviour = newBehaviour;
    }

    /**
     * Get the current behaviour on view resize.
     *
     * @return ResizeBehaviour
     */
    public ResizeBehaviour getResizeBehaviour() {
        return mResizeBehaviour;
    }


    /**
     * Cancel the last drawn segment
     */
    public void undoLast() {

        if (mPaths.size() > 0) {
            mFinishPath = true;
            invalidate();

            mCanceledPaths.add(mPaths.get(mPaths.size() - 1));
            mPaths.remove(mPaths.size() - 1);
            invalidate();

            notifyRedoUndoCountChanged();
        }
    }

    /**
     * Re-add the first removed path and redraw
     */
    public void redoLast() {

        if (mCanceledPaths.size() > 0) {
            mPaths.add(mCanceledPaths.get(mCanceledPaths.size() - 1));
            mCanceledPaths.remove(mCanceledPaths.size() - 1);
            invalidate();

            notifyRedoUndoCountChanged();
        }
    }

    /**
     * Remove all the paths and redraw (can be undone with {@link #redoLast()})
     */
    public void undoAll() {
        Collections.reverse(mPaths);
        mCanceledPaths.addAll(mPaths);
        mPaths = new ArrayList<>();
        invalidate();

        notifyRedoUndoCountChanged();
    }

    /**
     * Re-add all the removed paths and redraw
     */
    public void redoAll() {

        if (mCanceledPaths.size() > 0) {
            mPaths.addAll(mCanceledPaths);
            mCanceledPaths = new ArrayList<>();
            invalidate();

            notifyRedoUndoCountChanged();
        }
    }

    /**
     * Get how many undo operations are available
     *
     * @return int
     */
    public int getUndoCount() {
        return mPaths.size();
    }

    /**
     * Get how many redo operations are available
     *
     * @return int
     */
    public int getRedoCount() {
        return mCanceledPaths.size();
    }

    /**
     * Get how many paths are drawn on this FreeDrawView
     *
     * @param includeCurrentlyDrawingPath Include the path that is currently been drawn
     * @return The number of paths drawn
     */
    public int getPathCount(boolean includeCurrentlyDrawingPath) {
        int size = mPaths.size();

        if (includeCurrentlyDrawingPath && mPoints.size() > 0) {
            size++;
        }
        return size;
    }

    /**
     * Set a path drawn listener, will be called every time a new path is drawn.
     *
     * @param listener listener
     */
    public void setOnPathDrawnListener(PathDrawnListener listener) {
        mPathDrawnListener = listener;
    }

    /**
     * Remove the path drawn listener.
     */
    public void removePathDrawnListener() {
        mPathDrawnListener = null;
    }

    /**
     * Clear the current draw and the history.
     */
    public void clearDrawAndHistory() {

        clearDraw(false);
        clearHistory(true);
    }

    /**
     * Clear the current draw
     */
    public void clearDraw() {

        clearDraw(true);
    }

    private void clearDraw(boolean invalidate) {
        mPoints = new ArrayList<>();
        mPaths = new ArrayList<>();

        notifyRedoUndoCountChanged();

        if (invalidate) {
            invalidate();
        }
    }

    /**
     * Clear the history (paths that can be redone).
     */
    public void clearHistory() {
        clearHistory(true);
    }

    private void clearHistory(boolean invalidate) {
        mCanceledPaths = new ArrayList<>();

        notifyRedoUndoCountChanged();

        if (invalidate) {
            invalidate();
        }
    }

    /**
     * Set a redo-undo count change listener, this will be called every time undo or redo count
     * changes
     *
     * @param listener listener
     */
    public void setPathRedoUndoCountChangeListener(PathRedoUndoCountChangeListener listener) {
        mPathRedoUndoCountChangeListener = listener;
    }

    /**
     * Remove the redo-undo count listener
     */
    public void removePathRedoUndoCountChangeListener() {
        mPathRedoUndoCountChangeListener = null;
    }

    /**
     * Get a serializable object with all the needed info about the current draw and state
     *
     * @return A {@link FreeDrawSerializableState} containing all the needed data
     */
    public FreeDrawSerializableState getCurrentViewStateAsSerializable() {

        return new FreeDrawSerializableState(mCanceledPaths, mPaths, getPaintColor(),
                getPaintAlpha(), getPaintWidth(), getResizeBehaviour(),
                mLastDimensionW, mLastDimensionH);
    }

    /**
     * Restore the state of the draw from the given serializable state
     *
     * @param state A {@link FreeDrawSerializableState} containing all the draw and paint info,
     *              if null, nothing will be restored. Null sub fields will be ignored
     */
    public void restoreStateFromSerializable(FreeDrawSerializableState state) {

        if (state != null) {

            if (state.getCanceledPaths() != null) {
                mCanceledPaths = state.getCanceledPaths();
            }

            if (state.getPaths() != null) {
                mPaths = state.getPaths();
            }

            mPaintColor = state.getPaintColor();
            mPaintAlpha = state.getPaintAlpha();

            mCurrentPaint.setColor(new Color(state.getPaintColor()));
            mCurrentPaint.setAlpha(state.getPaintAlpha() / 255f);
            setPaintWidthPx(state.getPaintWidth());

            mResizeBehaviour = state.getResizeBehaviour();

            if (state.getLastDimensionW() >= 0) {
                mLastDimensionW = state.getLastDimensionW();
            }

            if (state.getLastDimensionH() >= 0) {
                mLastDimensionH = state.getLastDimensionH();
            }

            notifyRedoUndoCountChanged();
            invalidate();
        }
    }

    /**
     * Create a Bitmap with the content drawn inside the view
     *
     * @param listener listener
     */
    public void getDrawScreenshot(final DrawCreatorListener listener) {
        try {
            PixelMap.InitializationOptions opts = new PixelMap.InitializationOptions();
            opts.pixelFormat = PixelFormat.ARGB_8888;
            opts.size = new Size(getWidth(), getHeight());
            PixelMap pixelMap = PixelMap.create(opts);
            Texture texture = new Texture(pixelMap);
            Canvas canvas = new Canvas(texture);
            onDraw(this, canvas);
            getContext().getUITaskDispatcher().delayDispatch(() -> listener.onDrawCreated(pixelMap), 200);
        } catch (Exception e) {
            listener.onDrawCreationError();
        }
    }


    private void notifyPathStart() {
        if (mPathDrawnListener != null) {
            mPathDrawnListener.onPathStart();
        }
    }

    private void notifyPathDrawn() {
        if (mPathDrawnListener != null) {
            mPathDrawnListener.onNewPathDrawn();
        }
    }

    private void notifyRedoUndoCountChanged() {
        if (mPathRedoUndoCountChangeListener != null) {
            mPathRedoUndoCountChangeListener.onRedoCountChanged(getRedoCount());
            mPathRedoUndoCountChangeListener.onUndoCountChanged(getUndoCount());
        }
    }

    private void initPaints(AttrSet attrSet) {
        mCurrentPaint = FreeDrawHelper.createPaint();
        mCurrentPaint.setColor(new Color(AttrUtils.getColorFromAttr(attrSet, "paintColor", mPaintColor)));
        mCurrentPaint.setAlpha(AttrUtils.getIntegerFromAttr(attrSet, "paintAlpha", mPaintAlpha) / 255f);
        mCurrentPaint.setStrokeWidth(AttrUtils.getDimensionFromAttr(attrSet, "paintWidth",
                (int) FreeDrawHelper.convertDpToPixels(getContext(), DEFAULT_STROKE_WIDTH)));

        FreeDrawHelper.setupStrokePaint(mCurrentPaint);

        if (attrSet != null) {
            // CLEAR,FIT_XY,CROP
            String resizeBehaviourStr = AttrUtils.getStringFromAttr(attrSet, "resizeBehaviour", "");
            if (ResizeBehaviour.CLEAR.name().equalsIgnoreCase(resizeBehaviourStr)) {
                mResizeBehaviour = ResizeBehaviour.CLEAR;
            } else if (ResizeBehaviour.FIT_XY.name().equalsIgnoreCase(resizeBehaviourStr)) {
                mResizeBehaviour = ResizeBehaviour.FIT_XY;
            } else {
                mResizeBehaviour = ResizeBehaviour.CROP;
            }
        }
    }

    private Paint createAndCopyColorAndAlphaForFillPaint(Paint from, boolean copyWidth) {
        Paint paint = FreeDrawHelper.createPaint();
        FreeDrawHelper.setupFillPaint(paint);
        paint.setColor(from.getColor());
        paint.setAlpha(from.getAlpha());
        if (copyWidth) {
            paint.setStrokeWidth(from.getStrokeWidth());
        }
        return paint;
    }

    private void onDraw(Component var1, Canvas canvas) {
        if (mPaths.size() == 0 && mPoints.size() == 0) {
            return;
        }

        final boolean finishedPath = mFinishPath;
        mFinishPath = false;
        for (HistoryPath currentPath : mPaths) {
            if (currentPath.isPoint()) {
                canvas.drawCircle(currentPath.getOriginX(), currentPath.getOriginY(),
                        currentPath.getPaint().getStrokeWidth() / 2, currentPath.getPaint());
            } else {
                canvas.drawPath(currentPath.getPath(), currentPath.getPaint());
            }
        }

        if (mCurrentPath == null) {
            mCurrentPath = new Path();
        } else {
            mCurrentPath.rewind();
        }
        if (mPoints.size() == 1 || FreeDrawHelper.isAPoint(mPoints)) {
            canvas.drawCircle(mPoints.get(0).x, mPoints.get(0).y,
                    mCurrentPaint.getStrokeWidth() / 2,
                    createAndCopyColorAndAlphaForFillPaint(mCurrentPaint, false));
        } else if (mPoints.size() != 0) {
            boolean first = true;
            for (Point point : mPoints) {
                if (first) {
                    mCurrentPath.moveTo(point.x, point.y);
                    first = false;
                } else {
                    mCurrentPath.lineTo(point.x, point.y);
                }
            }
            canvas.drawPath(mCurrentPath, mCurrentPaint);
        }
        if (finishedPath && mPoints.size() > 0) {
            createHistoryPathFromPoints();
        }
    }

    private void createHistoryPathFromPoints() {
        mPaths.add(new HistoryPath(mPoints, new Paint(mCurrentPaint)));
        mPoints = new ArrayList<>();
        notifyPathDrawn();
        notifyRedoUndoCountChanged();
    }


    @Override
    public void onComponentBoundToWindow(Component component) {
        onSizeChanged(component.getWidth(), component.getHeight(), 0, 0);
        findScrollView(getComponentParent());
    }

    private void findScrollView(ComponentParent componentParent) {
        if (componentParent == null) {
            return;
        }
        Component parentComponent = (Component) componentParent;
        if (componentParent instanceof ScrollView) {
            mScrollView = (ScrollView) componentParent;
            return;
        }
        findScrollView(parentComponent.getComponentParent());
    }

    @Override
    public void onComponentUnboundFromWindow(Component component) {
    }

    private void onSizeChanged(int ww, int hh, int oldw, int oldh) {
        float xMultiplyFactor = 1;
        float yMultiplyFactor = 1;
        if (mLastDimensionW == -1) {
            mLastDimensionW = ww;
        }
        if (mLastDimensionH == -1) {
            mLastDimensionH = hh;
        }
        if (ww >= 0 && ww != oldw && ww != mLastDimensionW) {
            xMultiplyFactor = (float) ww / mLastDimensionW;
            mLastDimensionW = ww;
        }
        if (hh >= 0 && hh != oldh && hh != mLastDimensionH) {
            yMultiplyFactor = (float) hh / mLastDimensionH;
            mLastDimensionH = hh;
        }
        multiplyPathsAndPoints(xMultiplyFactor, yMultiplyFactor);
    }

    private void multiplyPathsAndPoints(float xMultiplyFactor, float yMultiplyFactor) {

        if ((xMultiplyFactor == 1 && yMultiplyFactor == 1)
                || (xMultiplyFactor <= 0 || yMultiplyFactor <= 0) ||
                (mPaths.size() == 0 && mCanceledPaths.size() == 0 && mPoints.size() == 0)) {
            return;
        }

        if (mResizeBehaviour == ResizeBehaviour.CLEAR) {
            mPaths = new ArrayList<>();
            mCanceledPaths = new ArrayList<>();
            mPoints = new ArrayList<>();
            return;
        } else if (mResizeBehaviour == ResizeBehaviour.CROP) {
            xMultiplyFactor = yMultiplyFactor = 1;
        }

        for (HistoryPath historyPath : mPaths) {

            if (historyPath.isPoint()) {
                historyPath.setOriginX(historyPath.getOriginX() * xMultiplyFactor);
                historyPath.setOriginY(historyPath.getOriginY() * yMultiplyFactor);
            } else {
                for (Point point : historyPath.getPoints()) {
                    point.x *= xMultiplyFactor;
                    point.y *= yMultiplyFactor;
                }
            }
            historyPath.generatePath();
        }

        for (HistoryPath historyPath : mCanceledPaths) {

            if (historyPath.isPoint()) {
                historyPath.setOriginX(historyPath.getOriginX() * xMultiplyFactor);
                historyPath.setOriginY(historyPath.getOriginY() * yMultiplyFactor);
            } else {
                for (Point point : historyPath.getPoints()) {
                    point.x *= xMultiplyFactor;
                    point.y *= yMultiplyFactor;
                }
            }
            historyPath.generatePath();
        }
        for (Point point : mPoints) {
            point.x *= xMultiplyFactor;
            point.y *= yMultiplyFactor;
        }
    }

    private boolean onTouchEvent(Component component, TouchEvent motionEvent) {
        if (motionEvent.getAction() == TouchEvent.PRIMARY_POINT_DOWN) {
            notifyPathStart();
        }
        if (mScrollView != null) {
            if (motionEvent.getAction() == TouchEvent.PRIMARY_POINT_DOWN ||
                    motionEvent.getAction() == TouchEvent.HOVER_POINTER_MOVE
            ) {
                mScrollView.setEnabled(false);
            } else if (motionEvent.getAction() == TouchEvent.PRIMARY_POINT_UP ||
                    motionEvent.getAction() == TouchEvent.CANCEL) {
                mScrollView.getContext().getUITaskDispatcher()
                        .delayDispatch(() -> mScrollView.setEnabled(true), 50);
            }
        }
        mCanceledPaths = new ArrayList<>();
        if ((motionEvent.getAction() != TouchEvent.PRIMARY_POINT_UP) &&
                (motionEvent.getAction() != TouchEvent.CANCEL)) {
            Point point;
            point = new Point();
            MmiPoint pointerPosition = motionEvent.getPointerPosition(motionEvent.getIndex());
            point.x = pointerPosition.getX();
            point.y = pointerPosition.getY();
            mPoints.add(point);
            mFinishPath = false;
        } else {
            mFinishPath = true;
        }
        invalidate();
        return true;
    }

    /**
     * DrawCreatorListener .
     */
    public interface DrawCreatorListener {
        /**
         * onDrawCreated .
         *
         * @param draw draw
         */
        void onDrawCreated(PixelMap draw);

        /**
         * onDrawCreationError.
         */
        void onDrawCreationError();
    }
}
